﻿using System;
using System.IO;
using System.ServiceModel;
using System.Threading;
using log4net;
using Needle.Bus.Engine.Factories;
using Needle.Bus.Engine.Filters;
using Needle.Bus.Engine.Management;
using Needle.Bus.Engine.MessageHandlers;
using Needle.Bus.Engine.Queues;
using Needle.Bus.Engine.Stages;
using Needle.Bus.Messages;
using Needle.Bus.Messages.Management;
using Needle.Utils;

namespace Needle.Bus.Engine
{

    public class Engine : EngineComponent, IBus
    {
        private ILog _log;
        private IScheduler _scheduler;
        private StageCollection _stages;
        private IFactory<IStage> _stageFactory;

        #region constructor

        public Engine(string id, IFactory<IStage> factory, IScheduler scheduler)
            : base(id)
        {
            if (String.IsNullOrEmpty(id))
                throw new ArgumentException("id is null or empty.", "id");
            if (factory == null)
                throw new ArgumentNullException("factory", "factory is null.");
            if (scheduler == null)
                throw new ArgumentNullException("scheduler", "scheduler is null.");

            _stageFactory = factory;
            _scheduler = scheduler;
            _log = LogManager.GetLogger(GetType().Name + " " + id);
            _stages = new StageCollection();
            _stages.StageOpening += OnComponentOpening;
            _stages.StageOpened += OnComponentOpened;
            _stages.StageClosing += OnComponentClosing;
            _stages.StageClosed += OnComponentClosed;
            _stages.StageFaulted += OnComponentFaulted;
            AttachStateEvents(_scheduler);
            AttachStateEvents(this);
        }

        #endregion

        public void StartStage(string id)
        {
            if (String.IsNullOrEmpty(id))
                throw new ArgumentException("id is null or empty.", "id");
            
            lock (ThisLock)
            {
                try
                {
                    if (_stages.Contains(id))
                        return;
                    switch (id)
                    {
                        case BusAddress.StageController:
                            StartStage(id, new StageController());
                            break;
                        case BusAddress.Router:
                            StartStage(id, new MessageRouter());
                            break;
                        case BusAddress.Tracer:
                            StartStage(id, new MessageTracer { Folder = Path.Combine(AppFileSystem.ExeFolder.FullName, "Logs") });
                            break;
                        case BusAddress.DeadMessages:
                            StartStage(id, new MessageDumper { Folder = Path.Combine(AppFileSystem.ExeFolder.FullName, "DeadMessages") });
                            break;
                        case BusAddress.PoisonMessages:
                            StartStage(id, new MessageDumper { Folder = Path.Combine(AppFileSystem.ExeFolder.FullName, "PoisonMessages") });
                            break;
                        case BusAddress.RejectedMessages:
                            StartStage(id, new MessageDumper { Folder = Path.Combine(AppFileSystem.ExeFolder.FullName, "RejectedMessages") });
                            break;
                        default:
                            StartStage(_stageFactory.Create(id));
                            break;
                    }
                }
                catch (Exception ex)
                {
                    _log.Error("Failed to start stage " + id, ex);
                    throw;
                }
            }
        }

        public void StartStage(string id, IMessageHandler handler)
        {
            if (String.IsNullOrEmpty(id))
                throw new ArgumentException("id is null or empty.", "id");
            if (handler == null)
                throw new ArgumentNullException("handler", "handler is null.");

            lock (ThisLock)
            {

                if (_stages.Contains(id))
                    throw new ArgumentException("Duplicate stage id: " + id);
               var stage = new Stage(id,
                                  new BufferQueue(id),
                                  new StageFilterBase(id + "Filter"),
                                  handler,
                                  new PerfCounter(id + "PerfCounter"));
                StartStage(stage);
            }
        }

        public void StopStage(string id)
        {
            if (String.IsNullOrEmpty(id))
                throw new ArgumentException("id is null or empty.", "id");
         
            lock (ThisLock)
            {
                if (_stages.Contains(id))
                {
                    var stage = _stages[id];
                    _scheduler.Unregister(stage);
                    if (stage.State != CommunicationState.Closed)
                        stage.Close();
                    _stages.Remove(stage);
                }
            }
        }

        public void Send(params IMessage[] messages)
        {
            Send(BusAddress.Router, messages);
        }

        public void Send(string address, params IMessage[] messages)
        {
            if (String.IsNullOrEmpty(address))
                throw new ArgumentException("address is null or empty.", "address");

            ValidateMessages(messages);

            IBufferQueue buffer = null;

            if (TryGetBuffer(address, false, out buffer))
            {
                buffer.Enqueue(messages);
            }
            else
            {
                try
                {
                    TryGetBuffer(BusAddress.DeadMessages, true, out buffer);

                    ErrorMessage[] deadMessages = Array.ConvertAll(messages,
                        m => ErrorMessage.Create(
                                     Bus.Context,
                                     DateTime.UtcNow,
                                     address,
                                     m,
                                     new UnknownAddressException(address)));

                    buffer.Enqueue(deadMessages);
                }
                catch (Exception ex)
                {
                    _log.Fatal(String.Format("Error delivering {0} dead messages to the dead-messages handler.", messages.Length), ex);
                    throw new UnknownAddressException(address, ex);
                }
                throw new UnknownAddressException(address);
            }
        }

        internal IStage GetStage(string id)
        {
            if (String.IsNullOrEmpty(id))
                throw new ArgumentException("id is null or empty.", "id");
         
            lock (ThisLock)
            {
                IStage stage = null;
                if (!_stages.TryGetValue(id, out stage))
                {
                    StartStage(id);
                    stage = _stages[id];
                }
                return stage;
            }
        }

        #region privates

        protected override void OnOpen(TimeSpan timeout)
        {
            _scheduler.Open(timeout);
        }

        protected override void OnAbort()
        {
            _scheduler.Abort();
            foreach (var stage in _stages)
                stage.Abort();
        }

        protected override void OnClose(TimeSpan timeout)
        {
            _scheduler.Close(timeout);
            foreach (var stage in _stages)
                stage.Close();
        }

        /// <summary>
        /// Should only be called while locked on ThisLock
        /// </summary>
        private void StartStage(IStage stage)
        {
            if (stage == null)
                throw new ArgumentNullException("stage", "stage is null.");
            if (stage.State != CommunicationState.Created)
                throw new ArgumentException("stage has invalid state, it must be in the Created state.");

           
                using (var bus = (new Bus(this)).SetCurrent())
                {
                    if (_stages.Contains(stage.Id))
                        throw new ArgumentException("Duplicate stage id: " + stage.Id);

                    _stages.Add(stage);
                    _scheduler.Register(stage);
                    stage.Open();
                }
         
        }

        private void ValidateMessages(IMessage[] messages)
        {
            if (messages == null || messages.Length == 0)
                throw new ArgumentNullException("No messages or a null message.");
            for (int i = 0; i < messages.Length; i++)
                if (messages[i] == null)
                    throw new ArgumentNullException(String.Format("messages[{0}] is null.", i));
        }

        private bool TryGetBuffer(string address, bool throwOnError, out IBufferQueue buffer)
        {
            buffer = null;
            try
            {
                var stage = GetStage(address);
                buffer = stage.Buffer;
            }
            catch (Exception ex)
            {
                _log.Error(String.Format("Getting buffer for stage {0} failed.", address), ex);

                if (throwOnError)
                    throw new UnknownAddressException(address, ex);
                else
                    return false;
            }
            return true;
        }

        private void AttachStateEvents(IEngineComponent component)
        {
            component.Opening += OnComponentOpening;
            component.Opened += OnComponentOpened;
            component.Closing += OnComponentClosing;
            component.Closed += OnComponentClosed;
            component.Faulted += OnComponentFaulted;
        }

        private void DetachStateEvents(IEngineComponent component)
        {
            component.Opening -= OnComponentOpening;
            component.Opened -= OnComponentOpened;
            component.Closing -= OnComponentClosing;
            component.Closed -= OnComponentClosed;
            component.Faulted -= OnComponentFaulted;
        }

        private void OnComponentOpening(object sender, EventArgs e)
        {
            if (!_log.IsDebugEnabled)
                return;
            var component = sender as IEngineComponent;
            if (component != null)
                _log.DebugFormat("Opening {0} {1}", component.GetType().Name, component.Id);
        }

        private void OnComponentOpened(object sender, EventArgs e)
        {
            if (!_log.IsDebugEnabled)
                return;
            var component = sender as IEngineComponent;
            if (component != null)
                _log.DebugFormat("Opened  {0} {1}", component.GetType().Name, component.Id);
        }

        private void OnComponentClosing(object sender, EventArgs e)
        {
            if (!_log.IsDebugEnabled)
                return;
            var component = sender as IEngineComponent;
            if (component != null)
                _log.DebugFormat("Closing {0} {1}", component.GetType().Name, component.Id);
        }

        private void OnComponentClosed(object sender, EventArgs e)
        {
            if (!_log.IsDebugEnabled)
                return;
            var component = sender as IEngineComponent;
            if (component != null)
                _log.DebugFormat("Closed  {0} {1}", component.GetType().Name, component.Id);
        }

        private void OnComponentFaulted(object sender, EventArgs e)
        {
            if (!_log.IsDebugEnabled)
                return;
            var component = sender as IEngineComponent;
            if (component != null)
                _log.DebugFormat("Faulted {0} {1}", component.GetType().Name, component.Id);
        }

        #endregion

    }

}
